跳到主要内容

使用 Flash 贷款从 Aave 免费借取贷款

你有没有想过成为一个亿万富翁而不需要抵押任何东西?那么,这就是闪光贷款的作用。

在这一关中,我们将学习如何从 Aave 获得闪电贷款,并利用 DeFi 中这个在 web2 世界中不存在的新概念。在传统的金融世界中没有很好的类比,因为这在区块链之外根本不可能。

传统的银行系统?

传统的银行系统是如何运作的?如果你想贷款,你必须提出一个抵押品,你可以用它来贷款。这通常是 DeFi 中借贷的方式。

然而,有时你可能需要一大笔钱来执行一些你不可能提供抵押品的攻击,也许是为了执行一个巨大的套利交易或攻击一些合约。

什么是 Flash 贷款?

你可能在想。它是某种贷款吗?嗯,是的,它是。这是一种特殊的贷款,只要借款人在交易结束前归还借款金额和一些利息,就可以借到资产。由于借款金额连同利息将在同一笔交易中归还,因此不存在任何人带着借来的钱逃跑的可能性。如果在同一笔交易中没有偿还贷款,交易就会整体失败并被退回。

这个简单但迷人的细节让你可以在没有预付资金或抵押品的情况下借到数十亿,因为你需要在同一笔交易中偿还。然而,在借钱和还钱之间,你可以疯狂地使用这些钱。

请记住,所有这些都发生在一次交易中

闪电贷款的应用

它们有助于资产之间的套利,在 DeFi 借贷协议中引起清算,经常在 DeFi 黑客中发挥作用,以及其他用例。你基本上可以利用你自己的创造力来创造新的东西。

在本教程中,我们将只关注简单闪存贷款的工作方式,包括能够借入一个资产。也有一些替代方案,你也可以借用多种资产。要了解其他类型的闪存贷款,请阅读 Aave 的文件。

让我们试着深入了解一个用例,那就是套利。什么是套利?想象一下,有两个加密货币交易所--A 和 B。现在 A 以比 B 更低的价格出售代币 LW3。如果你从 A 处购买 LW3 以换取 DAI,然后在 B 处出售,获得比你最初的数额更多的 DAI,你就可以获得利润。

交易所之间的价格差异交易被称为套利。套利者是一种必要的邪恶,有助于保持各交易所的价格一致。

闪电贷款如何运作?

任何闪电贷款都有 4 个基本步骤。为了执行闪电贷款,你首先需要编写一个智能合约,其中有一个使用闪电贷款的交易。假设该函数被称为 createFlashLoan()。当你调用该函数时,以下 4 个步骤依次发生。

  • 你的合约调用一个 Flash 贷款提供者的函数,比如 Aave,表明你想要哪种资产和多少资产
  • Flash 贷款提供者将资产发送给你的合约,并调用你的合约中的另一个函数,executeOperation
  • executeOperation 是你必须写的所有自定义代码--你在这里疯狂地用钱。最后,你批准 Flash 贷款提供者收回借来的资产,并支付保险费。
  • Flash 贷款提供者收回它给你的资产,以及溢价。

如果你看这张图,你可以看到闪光贷款是如何帮助用户在套利交易中获利的。最初,用户通过调用你的合约中的方法 createFlashLoan 开始交易,该方法被命名为 FlashLoan 合约。当用户调用这个函数时,你的合约会调用 Pool 合约,该合约暴露了给定资产池的流动性管理方法,并且已经被 Aave 部署。当 Pool Contract 收到创建 flash loan 的请求时,它调用你的合约上的 executeOperation 方法,并以用户要求的金额为 DAI。请注意,用户不需要提供任何抵押品来获得 DAI,他只需要调用交易,池塘合约要求你有 executeOperation 方法,它才能向你发送 DAI

现在在收到 DAI 后的 executeOperation 方法中,你可以调用交易所 A 的合约,从池子合约发给你的所有 DAI 中购买一些 LW3 代币。收到 LW3 代币后,你可以再次通过调用交易所 B 的合约将其换成 DAI。

这时你的合约已经获利,所以它可以允许 Pool 合约提取它发给我们合约的金额以及一些利息,并从 executeOperation 方法中返回。

一旦我们的合约从 executeOperation 方法中返回,Pool Contract 就可以提取它最初发送的 DAI 和 FlashLoan 合约的利息,所以它就提取了它。

所有这些都发生在一个交易中,如果在交易中没有得到满足,例如我们的合约在做套利时失败了,请记住所有的东西都会被还原,就像我们的合约一开始就没有得到 DAI 一样。你所损失的只是执行这一切的汽油费。

用户现在可以在交易完成后从合约中提取利润。

Aave 建议在套利成功后提取资金,不要将资金长期保留在合约中,因为这可能会导致悲痛的攻击。这里提供了一个例子。

构建

让我们建立一个例子,你可以体验一下我们如何开始一个闪电贷款。请注意,我们不会在这里实际进行套利,因为找到有利可图的套利机会是最难的部分,与代码无关,但基本上只是学习如何执行闪电贷款。

初始化项目

npm init --yes
npm install --save-dev hardhat
npm install --save-dev @nomicfoundation/hardhat-toolbox
npx hardhat

选择创建一个 Javascript 项目 对已经指定的 Hardhat 项目根目录按回车键 对于是否要添加.gitignore 的问题按回车键 在 "你是否想用 npm(@nomicfoundation/hardhat-toolbox)安装这个样本项目的依赖项? 现在你有一个准备好的 hardhat 项目了

安装依赖

在同一个终端上安装 OpenZeppelin 合约、Aave 合约和 dotenv

npm install @openzeppelin/contracts @aave/core-v3 dotenv

现在让我们开始编写我们的智能合约,在合约目录下创建你的第一个智能合约,并将其命名为 FlashLoanExample.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "@aave/core-v3/contracts/flashloan/base/FlashLoanSimpleReceiverBase.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";


contract FlashLoanExample is FlashLoanSimpleReceiverBase {
using SafeMath for uint;
event Log(address asset, uint val);

constructor(IPoolAddressesProvider provider)
FlashLoanSimpleReceiverBase(provider)
{}

function createFlashLoan(address asset, uint amount) external {
address receiver = address(this);
bytes memory params = ""; // use this to pass arbitrary data to executeOperation
uint16 referralCode = 0;

POOL.flashLoanSimple(
receiver,
asset,
amount,
params,
referralCode
);
}

function executeOperation(
address asset,
uint256 amount,
uint256 premium,
address initiator,
bytes calldata params
) external returns (bool){
// do things like arbitrage here
// abi.decode(params) to decode params

uint amountOwing = amount.add(premium);
IERC20(asset).approve(address(POOL), amountOwing);
emit Log(asset, amountOwing);
return true;
}
}

现在让我们试着分解这个契约并更好地理解它。当我们声明合约时,我们是这样做的:合约 FlashLoanExample 是 FlashLoanSimpleReceiverBase {,我们的合约被命名为 FlashLoanExample,它继承了一个名为 FlashLoanSimpleReceiverBase 的合约,这是一个来自 Aave 的合约,你用它来设置你的合约作为 Flash 贷款的接收器。

现在,在声明了合约之后,如果我们看一下构造函数,它接收了一个 IPoolAddressesProvider 类型的提供者,这基本上是我们在上面的例子中谈到的池子合约的地址,包裹着一个 IPoolAddressesProvider 类型的接口。这个接口也是由 Aave 提供给我们的,可以在这里找到。FlashLoanSimpleReceiverBase 在其构造函数中需要这个提供者。

constructor(IPoolAddressesProvider provider)
FlashLoanSimpleReceiverBase(provider)
{}

我们实现的第一个函数是 createFlashLoan,它从用户那里接收资产和金额,他想开始闪光贷款。现在对于接收者的地址,你可以指定 FlashLoanExample 合约的地址,我们没有参数,所以让我们保持为空。对于 referralCode,我们保持它为 0,因为这个交易是由用户直接执行的,没有任何中间人。要了解更多关于这些参数的信息,你可以到这里。在声明了这些变量之后,你可以在池塘合约的实例中调用 flashLoanSimple 方法,该方法在 FlashLoanSimpleReceiverBase 中被初始化,我们的合约已经继承了该方法,你可以看一下这里的代码。

createFlashLoan 方法

在调用 flashLoanSimple 后,Pool Contract 将进行一些检查,并将资产按要求的金额发送到 FlashLoanExample Contract,并调用 executeOperation 方法。在这个方法中,你可以对这个资产做任何事情,但是在这个合约中,我们只是批准 Pool Contract 提取我们所欠的金额和一些溢价。然后我们发出一个日志,并从该函数中返回。

executeOperation 方法

现在我们将尝试创建一个测试,以实际看到这个 Flash 贷款的运行。

现在,由于 Pool Contract 被部署在 Polygon Mainnet 上,我们需要一些方法在测试中与它互动。

我们使用 Hardhat 的一个功能,即 Mainnet Forking,它可以模拟拥有与 mainnet 相同的状态,但它将作为一个本地开发网络工作。这样你就可以与部署的协议进行交互,并在本地测试复杂的交互。

注意,这是从 Hardhat 的官方文档中引用的

要配置这个,打开你的 hardhat.config.js,用以下几行代码替换其已有的内容

require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config({ path: ".env" });

const QUICKNODE_RPC_URL = process.env.QUICKNODE_RPC_URL;

/**
* @type import('hardhat/config').HardhatUserConfig
*/
module.exports = {
solidity: "0.8.10",
networks: {
hardhat: {
forking: {
url: QUICKNODE_RPC_URL,
},
},
},
};

.env 配置

QUICKNODE_RPC_URL="QUICKNODE-RPC-URL-FOR-POLYGON-MAINNET"

将 QUICKNODE-RPC-URL-FOR-POLYGON-MAINNET 替换为 Polygon Mainnet 的节点的 URL。为了得到这个 URL,请进入 Quicknode 并登录。然后点击创建一个端点,选择 Polygon 链和 Mainnet 网络。点击 "继续",在 "发现 "模式下创建应用程序,以保持在免费层上。从你的仪表板上复制 HTTP 提供者链接,并将其添加到环境文件中。

创建.env 文件后,在我们实际编写测试之前,你还需要一个文件。

创建一个名为 config.js 的新文件,并在其中添加以下几行代码

// Mainnet DAI Address
const DAI = "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063";
// Random user's address that happens to have a lot of DAI on Polygon Mainnet
const DAI_WHALE = "0xdfD74E3752c187c4BA899756238C76cbEEfa954B";

// Mainnet Pool contract address
const POOL_ADDRESS_PROVIDER = "0xa97684ead0e402dc232d5a977953df7ecbab3cdb";
module.exports = {
DAI,
DAI_WHALE,
POOL_ADDRESS_PROVIDER,
};

如果你看一下这个文件,我们有三个变量--DAI、DAI_WHALE 和 POOL_ADDRESS_PROVIDER。DAI 是多边形主网上的 DAI 合约的地址。DAI_WHALE 是 polygon mainnet 上有很多 DAI 的地址,POOL_ADDRESS_PROVIDER 是 polygon mainnet 上 PoolAddressesProvider 的地址,我们的合约在构造函数中期待这个地址。这个地址可以在这里找到。

由于我们没有实际执行任何套利,因此,如果我们按原样运行合约,将无法支付溢价,我们使用另一个 Hardhat 功能,称为冒充,让我们代表任何地址发送交易,即使没有他们的私钥。然而,当然,这只在本地开发网络上起作用,而不是在真实网络上。使用冒名顶替,我们将从 DAI_WHALE 那里偷取一些 DAI,这样我们就有足够的 DAI 来用溢价偿还贷款。

真棒 🚀,我们已经设置好了一切,现在让我们继续写测试。

在你的测试文件夹中创建一个新的文件 deploy.js,并在其中添加以下几行代码。

const { expect, assert } = require("chai");
const { BigNumber } = require("ethers");
const { ethers, waffle, artifacts } = require("hardhat");
const hre = require("hardhat");

const { DAI, DAI_WHALE, POOL_ADDRESS_PROVIDER } = require("../config");

describe("Deploy a Flash Loan", function () {
it("Should take a flash loan and be able to return it", async function () {
const flashLoanExample = await ethers.getContractFactory(
"FlashLoanExample"
);

const _flashLoanExample = await flashLoanExample.deploy(
// Address of the PoolAddressProvider: you can find it here: https://docs.aave.com/developers/deployed-contracts/v3-mainnet/polygon
POOL_ADDRESS_PROVIDER
);
await _flashLoanExample.deployed();

const token = await ethers.getContractAt("IERC20", DAI);
const BALANCE_AMOUNT_DAI = ethers.utils.parseEther("2000");

// Impersonate the DAI_WHALE account to be able to send transactions from that account
await hre.network.provider.request({
method: "hardhat_impersonateAccount",
params: [DAI_WHALE],
});
const signer = await ethers.getSigner(DAI_WHALE);
await token
.connect(signer)
.transfer(_flashLoanExample.address, BALANCE_AMOUNT_DAI); // Sends our contract 2000 DAI from the DAI_WHALE

const tx = await _flashLoanExample.createFlashLoan(DAI, 1000); // Borrow 1000 DAI in a Flash Loan with no upfront collateral
await tx.wait();
const remainingBalance = await token.balanceOf(_flashLoanExample.address); // Check the balance of DAI in the Flash Loan contract afterwards
expect(remainingBalance.lt(BALANCE_AMOUNT_DAI)).to.be.true; // We must have less than 2000 DAI now, since the premium was paid from our contract's balance
});
});

现在让我们试着理解这几行代码中所发生的事情。

首先,使用 Hardhat 的扩展以太坊版本,我们调用函数 getContractAt 来获取部署在 Polygon Mainnet 上的 DAI 的实例。记住 Hardhat 将模拟 Polygon Mainnet,所以当你在 config.js 中指定的 DAI 的地址上得到合约时,Hardhat 实际上将创建一个与 Polygon Mainnet 相匹配的 DAI 合约实例。

之后,下面的几行将再次尝试用 DAI_WHALE 的地址冒充/模拟 Polygon Mainnet 上的账户。现在迷人的一点是,即使 Hardhat 在本地测试环境中没有 DAI_WHALE 的私钥,它也会表现得好像我们已经知道它的私钥,并且可以代表 DAI_WHALE 签署交易。它也将拥有它在多边形主网上的 DAI 数量。

await hre.network.provider.request({
method: "hardhat_impersonateAccount",
params: [DAI_WHALE],
});

现在我们为 DAI_WHALE 创建一个签名者,这样我们就可以用 DAI_WHALE 的地址调用模拟 DAI 合约,并将一些 DAI 转移到 FlashLoanExample 合约。我们需要这样做,这样我们就可以用保险费来偿还贷款,否则我们将无法支付保险费。在现实世界的应用中,溢价将从套利或攻击智能合约的利润中支付。

const signer = await ethers.getSigner(DAI_WHALE);
await token
.connect(signer)
.transfer(_flashLoanExample.address, BALANCE_AMOUNT_DAI);

在这之后,我们开始一个闪电贷款,并检查 FlashLoanExampleContract 的剩余余额是否少于它最初开始的金额,金额会更少,因为合约必须支付贷款金额的溢价。

const tx = await _flashLoanExample.createFlashLoan(DAI, 1000);
await tx.wait();
const remainingBalance = await token.balanceOf(_flashLoanExample.address);
expect(remainingBalance.lt(BALANCE_AMOUNT_DAI)).to.be.true;

要运行该测试,你可以在终端上简单地执行。

npx hardhat test

如果测试通过,你就成功地执行了闪电贷款。